package info.thereisonlywe.core.audio;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import javax.sound.sampled.AudioFileFormat;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Control;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.FloatControl;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.UnsupportedAudioFileException;

import javazoom.jlgui.basicplayer.BasicController;
import javazoom.jlgui.basicplayer.BasicPlayerException;
import javazoom.jlgui.basicplayer.BasicPlayerListener;

public class AudioPlayerModel implements BasicController, Runnable {
	protected int EXTERNAL_BUFFER_SIZE = 4000 * 4;
	protected int SKIP_INACCURACY_SIZE = 1200;

	private int encodedBytes;

	protected Thread m_thread = null;
	protected Object m_dataSource;
	protected AudioInputStream m_encodedaudioInputStream;
	protected int encodedLength = -1;
	protected AudioInputStream m_audioInputStream;
	protected AudioFileFormat m_audioFileFormat;
	protected SourceDataLine m_line;
	protected FloatControl m_gainControl;
	protected FloatControl m_panControl;

	private int lineBufferSize = -1;
	private long threadSleep = -1;

	/**
	 * These variables are used to distinguish stopped, paused, playing states.
	 * We need them to control Thread.
	 */
	public static final int UNKNOWN = -1;
	public static final int PLAYING = 0;
	public static final int PAUSED = 1;
	public static final int STOPPED = 2;
	public static final int OPENED = 3;
	public static final int SEEKING = 4;

	private int m_status = UNKNOWN;
	// Listeners to be notified.
	private Collection m_listeners = null;

	private final Map empty_map = new HashMap();

	/**
	 * Constructs a Basic Player.
	 */
	public AudioPlayerModel()
	{
		m_dataSource = null;
		m_listeners = new ArrayList();
		reset();
	}

	public File getFile()
	{
		return (File) m_dataSource;
	}

	public int getEncodedBytes()
	{
		return encodedBytes;
	}

	public int getBytesToBeProcessed()
	{
		if (m_line == null) return -1;
		else return m_line.available();
	}

	protected void reset()
	{
		m_status = UNKNOWN;
		if (m_audioInputStream != null)
		{
			synchronized (m_audioInputStream)
			{
				closeStream();
			}
		}
		m_audioInputStream = null;
		m_audioFileFormat = null;
		m_encodedaudioInputStream = null;
		encodedLength = -1;
		if (m_line != null)
		{
			m_line.stop();
			m_line.close();
			m_line = null;
		}
		m_gainControl = null;
		m_panControl = null;
		encodedBytes = 0;
	}

	/**
	 * Add listener to be notified.
	 * 
	 * @param bpl
	 */
	public void addBasicPlayerListener(BasicPlayerListener bpl)
	{
		m_listeners.add(bpl);
	}

	/**
	 * Return registered listeners.
	 * 
	 * @return
	 */
	public Collection getListeners()
	{
		return m_listeners;
	}

	/**
	 * Remove registered listener.
	 * 
	 * @param bpl
	 */
	public void removeBasicPlayerListener(BasicPlayerListener bpl)
	{
		if (m_listeners != null)
		{
			m_listeners.remove(bpl);
		}
	}

	/**
	 * Set SourceDataLine buffer size. It affects audio latency. (the delay
	 * between line.write(data) and real sound). Minimum value should be over
	 * 10000 bytes.
	 * 
	 * @param size
	 *            -1 means maximum buffer size available.
	 */
	public void setLineBufferSize(int size)
	{
		lineBufferSize = size;
	}

	/**
	 * Return SourceDataLine buffer size.
	 * 
	 * @return -1 maximum buffer size.
	 */
	public int getLineBufferSize()
	{
		return lineBufferSize;
	}

	/**
	 * Set thread sleep time. Default is -1 (no sleep time).
	 * 
	 * @param time
	 *            in milliseconds.
	 */
	public void setSleepTime(long time)
	{
		threadSleep = time;
	}

	/**
	 * Return thread sleep time in milliseconds.
	 * 
	 * @return -1 means no sleep time.
	 */
	public long getSleepTime()
	{
		return threadSleep;
	}

	/**
	 * Returns PlayerModel status.
	 * 
	 * @return status
	 */
	public int getStatus()
	{
		return m_status;
	}

	/**
	 * Open file to play.
	 */
	@Override
	public void open(File file) throws BasicPlayerException
	{
		// log.info("open("+file+")");
		if (file != null)
		{
			m_dataSource = file;
			initAudioInputStream();
		}
	}

	/**
	 * Open URL to play.
	 */
	@Override
	public void open(URL url) throws BasicPlayerException
	{
		// log.info("open("+url+")");
		if (url != null)
		{
			m_dataSource = url;
			initAudioInputStream();
		}
	}

	/**
	 * Open inputstream to play.
	 */
	@Override
	public void open(InputStream inputStream) throws BasicPlayerException
	{
		// log.info("open("+inputStream+")");
		if (inputStream != null)
		{
			m_dataSource = inputStream;
			initAudioInputStream();
		}
	}

	/**
	 * Inits AudioInputStream and AudioFileFormat from the data source.
	 * 
	 * @throws BasicPlayerException
	 */
	protected void initAudioInputStream() throws BasicPlayerException
	{
		try
		{
			reset();
			if (m_dataSource instanceof URL)
			{
				initAudioInputStream((URL) m_dataSource);
			}
			else if (m_dataSource instanceof File)
			{
				initAudioInputStream((File) m_dataSource);
			}
			else if (m_dataSource instanceof InputStream)
			{
				initAudioInputStream((InputStream) m_dataSource);
			}
			createLine();
			m_status = OPENED;
		}
		catch (LineUnavailableException e)
		{
			throw new BasicPlayerException(e);
		}
		catch (UnsupportedAudioFileException e)
		{
			throw new BasicPlayerException(e);
		}
		catch (IOException e)
		{
			throw new BasicPlayerException(e);
		}
	}

	/**
	 * Inits Audio ressources from file.
	 */
	protected void initAudioInputStream(File file)
			throws UnsupportedAudioFileException, IOException
	{
		m_audioInputStream = AudioSystem.getAudioInputStream(file);
		m_audioFileFormat = AudioSystem.getAudioFileFormat(file);
	}

	/**
	 * Inits Audio ressources from URL.
	 */
	protected void initAudioInputStream(URL url)
			throws UnsupportedAudioFileException, IOException
	{
		m_audioInputStream = AudioSystem.getAudioInputStream(url);
		m_audioFileFormat = AudioSystem.getAudioFileFormat(url);
	}

	/**
	 * Inits Audio ressources from InputStream.
	 */
	protected void initAudioInputStream(InputStream inputStream)
			throws UnsupportedAudioFileException, IOException
	{
		m_audioInputStream = AudioSystem.getAudioInputStream(inputStream);
		m_audioFileFormat = AudioSystem.getAudioFileFormat(inputStream);
	}

	/**
	 * Inits Audio ressources from AudioSystem.<br>
	 */
	protected void initLine() throws LineUnavailableException
	{
		// log.info("initLine()");
		if (m_line == null) createLine();
		if (!m_line.isOpen())
		{
			openLine();
		}
		else
		{
			AudioFormat lineAudioFormat = m_line.getFormat();
			AudioFormat audioInputStreamFormat = m_audioInputStream == null ? null
					: m_audioInputStream.getFormat();
			if (!lineAudioFormat.equals(audioInputStreamFormat))
			{
				m_line.close();
				openLine();
			}
		}
	}

	/**
	 * Inits a DateLine.<br>
	 * 
	 * We check if the line supports Gain and Pan controls.
	 * 
	 * From the AudioInputStream, i.e. from the sound file, we fetch information
	 * about the format of the audio data. These information include the
	 * sampling frequency, the number of channels and the size of the samples.
	 * There information are needed to ask JavaSound for a suitable output line
	 * for this audio file. Furthermore, we have to give JavaSound a hint about
	 * how big the internal buffer for the line should be. Here, we say
	 * AudioSystem.NOT_SPECIFIED, signaling that we don't care about the exact
	 * size. JavaSound will use some default value for the buffer size.
	 */
	protected void createLine() throws LineUnavailableException
	{
		// log.info("Create Line");
		if (m_line == null)
		{
			AudioFormat sourceFormat = m_audioInputStream.getFormat();
			// log.info("Create Line : Source format : " +
			// sourceFormat.toString());
			int nSampleSizeInBits = sourceFormat.getSampleSizeInBits();
			if (nSampleSizeInBits <= 0) nSampleSizeInBits = 16;
			if ((sourceFormat.getEncoding() == AudioFormat.Encoding.ULAW)
					|| (sourceFormat.getEncoding() == AudioFormat.Encoding.ALAW))
				nSampleSizeInBits = 16;
			if (nSampleSizeInBits != 8) nSampleSizeInBits = 16;
			AudioFormat targetFormat = new AudioFormat(
					AudioFormat.Encoding.PCM_SIGNED,
					sourceFormat.getSampleRate(), nSampleSizeInBits,
					sourceFormat.getChannels(), sourceFormat.getChannels()
							* (nSampleSizeInBits / 8),
					sourceFormat.getSampleRate(), false);
			// log.info("Create Line : Target format: " + targetFormat);
			// Keep a reference on encoded stream to progress
			// notification.
			m_encodedaudioInputStream = m_audioInputStream;
			try
			{
				// Get total length in bytes of the encoded stream.
				encodedLength = m_encodedaudioInputStream.available();
			}
			catch (IOException e)
			{
				// log.error("Cannot get m_encodedaudioInputStream.available()",e);
			}
			// Create decoded stream.
			m_audioInputStream = AudioSystem.getAudioInputStream(targetFormat,
					m_audioInputStream);
			AudioFormat audioFormat = m_audioInputStream.getFormat();
			DataLine.Info info = new DataLine.Info(SourceDataLine.class,
					audioFormat, AudioSystem.NOT_SPECIFIED);
			m_line = (SourceDataLine) AudioSystem.getLine(info);
			// log.debug("Line AudioFormat: " +
			// m_line.getFormat().toString());

			/*-- Display supported controls --*/
			/*
			 * Control[] c = m_line.getControls(); for (int p = 0; p < c.length;
			 * p++) { log.debug("Controls : " + c[p].toString()); }
			 */

			/*-- Is Gain Control supported ? --*/
			/*
			 * if (m_line.isControlSupported(FloatControl.Type.MASTER_GAIN )) {
			 * m_gainControl = (FloatControl)
			 * m_line.getControl(FloatControl.Type.MASTER_GAIN); //
			 * log.info("Master Gain Control : [" + m_gainControl.getMinimum() +
			 * "," + m_gainControl.getMaximum() + "] " +
			 * m_gainControl.getPrecision()); }
			 */

			/*-- Is Pan control supported ? --*/
			/*
			 * if (m_line.isControlSupported(FloatControl.Type.PAN)) {
			 * m_panControl = (FloatControl)
			 * m_line.getControl(FloatControl.Type.PAN); //
			 * log.info("Pan Control : [" + m_panControl.getMinimum() + "," +
			 * m_panControl.getMaximum() + "] " + m_panControl.getPrecision());
			 * }
			 */
		}
	}

	/**
	 * Opens the line.
	 */
	protected void openLine() throws LineUnavailableException
	{
		if (m_line != null)
		{
			AudioFormat audioFormat = m_audioInputStream.getFormat();
			int buffersize = lineBufferSize;
			if (buffersize <= 0) buffersize = m_line.getBufferSize();
			m_line.open(audioFormat, buffersize);
			// log.info("Open Line : BufferSize="+buffersize);

			/*-- Display supported controls --*/
			Control[] c = m_line.getControls();
			for (int p = 0; p < c.length; p++)
			{
				// log.debug("Controls : " + c[p].toString());
			}

			/*-- Is Gain Control supported ? --*/
			if (m_line.isControlSupported(FloatControl.Type.MASTER_GAIN))
			{
				m_gainControl = (FloatControl) m_line
						.getControl(FloatControl.Type.MASTER_GAIN);
				// log.info("Master Gain Control : [" +
				// m_gainControl.getMinimum() + "," +
				// m_gainControl.getMaximum()
				// + "] " + m_gainControl.getPrecision());
			}

			/*-- Is Pan control supported ? --*/
			if (m_line.isControlSupported(FloatControl.Type.PAN))
			{
				m_panControl = (FloatControl) m_line
						.getControl(FloatControl.Type.PAN);
				// log.info("Pan Control : [" +
				// m_panControl.getMinimum() + ","
				// + m_panControl.getMaximum() + "] " +
				// m_panControl.getPrecision());
			}
		}
	}

	/**
	 * Stops the playback.<br>
	 * 
	 * Player Status = STOPPED.<br>
	 * Thread should free Audio ressources.
	 */
	protected void stopPlayback()
	{
		if ((m_status == PLAYING) || (m_status == PAUSED))
		{
			if (m_line != null)
			{
				m_line.flush();
				m_line.stop();
			}
			m_status = STOPPED;
			synchronized (m_audioInputStream)
			{
				closeStream();
			}
			// log.info("stopPlayback() completed");
		}
	}

	/**
	 * Pauses the playback.<br>
	 * 
	 * Player Status = PAUSED.
	 */
	protected void pausePlayback()
	{
		if (m_line != null)
		{
			if (m_status == PLAYING)
			{
				m_line.flush();
				m_line.stop();
				m_status = PAUSED;
			}
		}
	}

	/**
	 * Resumes the playback.<br>
	 * 
	 * Player Status = PLAYING.
	 */
	protected void resumePlayback()
	{
		if (m_line != null)
		{
			if (m_status == PAUSED)
			{
				m_line.start();
				m_status = PLAYING;
				// log.info("resumePlayback() completed");
			}
		}
	}

	/**
	 * Starts playback.
	 */
	protected void startPlayback() throws BasicPlayerException
	{
		if (m_status == STOPPED) initAudioInputStream();
		if (m_status == OPENED)
		{
			// log.info("startPlayback called");
			if (!(m_thread == null || !m_thread.isAlive()))
			{
				// log.info("WARNING: old thread still running!!");
				int cnt = 0;
				while (m_status != OPENED)
				{
					try
					{
						if (m_thread != null)
						{
							// log.info("Waiting ... " + cnt);
							cnt++;
							Thread.sleep(1000);
							if (cnt > 2)
							{
								m_thread.interrupt();
							}
						}
					}
					catch (InterruptedException e)
					{
						throw new BasicPlayerException(
								BasicPlayerException.WAITERROR, e);
					}
				}
			}
			// Open SourceDataLine.
			try
			{
				initLine();
			}
			catch (LineUnavailableException e)
			{
				throw new BasicPlayerException(
						BasicPlayerException.CANNOTINITLINE, e);
			}

			// log.info("Creating new thread");
			m_thread = new Thread(this);
			m_thread.start();
			if (m_line != null)
			{
				m_line.start();
				m_status = PLAYING;
			}
		}
	}

	/**
	 * Main loop.
	 * 
	 * Player Status == STOPPED || SEEKING => End of Thread + Freeing Audio
	 * Ressources.<br>
	 * Player Status == PLAYING => Audio stream data sent to Audio line.<br>
	 * Player Status == PAUSED => Waiting for another status.
	 */
	@Override
	public void run()
	{
		// log.info("Thread Running");
		int nBytesRead = 1;
		byte[] abData = new byte[EXTERNAL_BUFFER_SIZE];
		// Lock stream while playing.
		synchronized (m_audioInputStream)
		{
			// Main play/pause loop.
			while ((nBytesRead != -1) && (m_status != STOPPED)
					&& (m_status != SEEKING) && (m_status != UNKNOWN))
			{
				if (m_status == PLAYING)
				{
					// Play.
					try
					{
						nBytesRead = m_audioInputStream.read(abData, 0,
								abData.length);
						if (nBytesRead >= 0)
						{
							byte[] pcm = new byte[nBytesRead];
							System.arraycopy(abData, 0, pcm, 0, nBytesRead);
							m_line.write(abData, 0, nBytesRead);
							encodedBytes = getEncodedStreamPosition();
						}
					}
					catch (IOException e)
					{
						// log.error("Thread cannot run()",e);
						m_status = STOPPED;
					}
					// Nice CPU usage.
					if (threadSleep > 0)
					{
						try
						{
							Thread.sleep(threadSleep);
						}
						catch (InterruptedException e)
						{
							// log.error("Thread cannot sleep("+threadSleep+")",e);
						}
					}
				}
				else
				{
					// Pause
					try
					{
						Thread.sleep(1000);
					}
					catch (InterruptedException e)
					{
						// log.error("Thread cannot sleep(1000)",e);
					}
				}
			}
			// Free audio resources.
			if (m_line != null)
			{
				m_line.drain();
				m_line.stop();
				m_line.close();
				m_line = null;
			}
			// Notification of "End Of Media"
			if (nBytesRead == -1)
			{
			}
			// Close stream.
			closeStream();
		}
		m_status = STOPPED;
		// log.info("Thread completed");
	}

	/**
	 * Skip bytes in the File inputstream. It will skip N frames matching to
	 * bytes, so it will never skip given bytes length exactly.
	 * 
	 * @param bytes
	 * @return value>0 for File and value=0 for URL and InputStream
	 * @throws BasicPlayerException
	 */
	protected long skipBytes(long bytes) throws BasicPlayerException
	{
		long totalSkipped = 0;
		if (m_dataSource instanceof File)
		{
			// log.info("Bytes to skip : "+bytes);
			int previousStatus = m_status;
			m_status = SEEKING;
			long skipped = 0;
			try
			{
				synchronized (m_audioInputStream)
				{
					initAudioInputStream();
					if (m_audioInputStream != null)
					{
						// Loop until bytes are really skipped.
						while (totalSkipped < (bytes - SKIP_INACCURACY_SIZE))
						{
							skipped = m_audioInputStream.skip(bytes
									- totalSkipped);
							totalSkipped = totalSkipped + skipped;
							// log.info("Skipped : "+totalSkipped+"/"+bytes);
							if (totalSkipped == -1)
								throw new BasicPlayerException(
										BasicPlayerException.SKIPNOTSUPPORTED);
						}
					}
				}
				m_status = OPENED;
				if (previousStatus == PLAYING) startPlayback();
				else if (previousStatus == PAUSED)
				{
					startPlayback();
					pausePlayback();
				}
			}
			catch (IOException e)
			{
				throw new BasicPlayerException(e);
			}
		}
		return totalSkipped;
	}

	protected int getEncodedStreamPosition()
	{
		int nEncodedBytes = -1;
		if (m_dataSource instanceof File)
		{
			try
			{
				if (m_encodedaudioInputStream != null)
				{
					nEncodedBytes = encodedLength
							- m_encodedaudioInputStream.available();
				}
			}
			catch (IOException e)
			{
			}
		}
		return nEncodedBytes;
	}

	protected void closeStream()
	{
		// Close stream.
		try
		{
			if (m_audioInputStream != null)
			{
				m_audioInputStream.close();
				// log.info("Stream closed");
			}

		}
		catch (IOException e)
		{
			// log.info("Cannot close stream",e);
		}
	}

	/**
	 * Returns true if Gain control is supported.
	 */
	public boolean hasGainControl()
	{
		if (m_gainControl == null)
		{
			// Try to get Gain control again (to support J2SE 1.5)
			if (m_line != null
					&& m_line.isControlSupported(FloatControl.Type.MASTER_GAIN))
				m_gainControl = (FloatControl) m_line
						.getControl(FloatControl.Type.MASTER_GAIN);
		}
		return m_gainControl != null;
	}

	/**
	 * Returns Gain value.
	 */
	public float getGainValue()
	{
		if (hasGainControl())
		{
			return m_gainControl.getValue();
		}
		else
		{
			return 0.0F;
		}
	}

	/**
	 * Gets max Gain value.
	 */
	public float getMaximumGain()
	{
		if (hasGainControl())
		{
			return m_gainControl.getMaximum();
		}
		else
		{
			return 0.0F;
		}
	}

	/**
	 * Gets min Gain value.
	 */
	public float getMinimumGain()
	{
		if (hasGainControl())
		{
			return m_gainControl.getMinimum();
		}
		else
		{
			return 0.0F;
		}
	}

	/**
	 * Returns true if Pan control is supported.
	 */
	public boolean hasPanControl()
	{
		if (m_panControl == null)
		{
			// Try to get Pan control again (to support J2SE 1.5)
			if (m_line.isControlSupported(FloatControl.Type.PAN))
				m_panControl = (FloatControl) m_line
						.getControl(FloatControl.Type.PAN);
		}
		return m_panControl != null;
	}

	/**
	 * Returns Pan precision.
	 */
	public float getPrecision()
	{
		if (hasPanControl())
		{
			return m_panControl.getPrecision();
		}
		else
		{
			return 0.0F;
		}
	}

	/**
	 * Returns Pan value.
	 */
	public float getPan()
	{
		if (hasPanControl())
		{
			return m_panControl.getValue();
		}
		else
		{
			return 0.0F;
		}
	}

	/**
	 * Deep copy of a Map.
	 * 
	 * @param src
	 * @return
	 */
	protected Map deepCopy(Map src)
	{
		HashMap map = new HashMap();
		if (src != null)
		{
			Iterator it = src.keySet().iterator();
			while (it.hasNext())
			{
				Object key = it.next();
				Object value = src.get(key);
				map.put(key, value);
			}
		}
		return map;
	}

	/**
	 * @see javazoom.jlgui.basicplayer.BasicController#seek(long)
	 */
	@Override
	public long seek(long bytes) throws BasicPlayerException
	{
		return skipBytes(bytes);
	}

	/**
	 * @see javazoom.jlgui.basicplayer.BasicController#play()
	 */
	@Override
	public void play() throws BasicPlayerException
	{
		startPlayback();
	}

	/**
	 * @see javazoom.jlgui.basicplayer.BasicController#stop()
	 */
	@Override
	public void stop() throws BasicPlayerException
	{
		stopPlayback();
	}

	/**
	 * @see javazoom.jlgui.basicplayer.BasicController#pause()
	 */
	@Override
	public void pause() throws BasicPlayerException
	{
		pausePlayback();
	}

	/**
	 * @see javazoom.jlgui.basicplayer.BasicController#resume()
	 */
	@Override
	public void resume() throws BasicPlayerException
	{
		resumePlayback();
	}

	/**
	 * Sets Pan value. Line should be opened before calling this method. Linear
	 * scale : -1.0 <--> +1.0
	 */
	@Override
	public void setPan(double fPan) throws BasicPlayerException
	{
		if (hasPanControl())
		{
			m_panControl.setValue((float) fPan);
		}
		else throw new BasicPlayerException(
				BasicPlayerException.PANCONTROLNOTSUPPORTED);
	}

	/**
	 * Sets Gain value. Line should be opened before calling this method. Linear
	 * scale 0.0 <--> 1.0 Threshold Coef. : 1/2 to avoid saturation.
	 */
	@Override
	public void setGain(double fGain) throws BasicPlayerException
	{
		if (hasGainControl())
		{
			double minGainDB = getMinimumGain();
			double ampGainDB = ((10.0f / 20.0f) * getMaximumGain())
					- getMinimumGain();
			double cste = Math.log(10.0) / 20;
			double valueDB = minGainDB + (1 / cste)
					* Math.log(1 + (Math.exp(cste * ampGainDB) - 1) * fGain);
			// log.debug("Gain : "+valueDB);
			m_gainControl.setValue((float) valueDB);
		}
		else throw new BasicPlayerException(
				BasicPlayerException.GAINCONTROLNOTSUPPORTED);
	}

}